package commandlineinterface;

import modeller.databasedesignmodel.*;
import modeller.databasedesignmodel.query.*;
import modeller.databasedesignmodel.relation.Relation;
import modeller.databasedesignmodel.relation.index.*;
import modeller.databasedesignmodel.transaction.*;
import storage.DesignSave;
import queped.SerializablePair;

import java.io.*;
import java.util.*;

/**
 * Created by:  Jason Ye
 * Date:        28/02/2012
 * Time:        13:19
 */
public class UIEvaluator {

    private int currentTransactionID = 1;

    private HashSet<Relation> relations;
    private HashSet<Transaction> transactions;
    private DatabaseDesign design;
    private Scanner scanner;
    private DesignSave saver;

    private int dbPageSize;
    private double dbFillFactor;

    private String[] yesNoArray = {"No", "Yes"};
    String[] sqlStatementTypesArray = {
            "Sequential Scan",
            "Scan with Equality Condition",
            "Scan with Range Condition",
            "Update/Delete Statement",
            "Insert Statement"
    };

    public UIEvaluator() {
        // instantiate database design and scanner
        design = new DatabaseDesign();
        scanner = new Scanner(System.in);
        transactions = new HashSet<Transaction>();
        relations = new HashSet<Relation>();
    }

    public DatabaseDesign initDatabaseDesign() {

        String[] options = {"Create one from scratch", "Load DatabaseDesign from file"};

        int i = getOptionFromUserInput("Would you like to load a database design, or create a new one?", options, scanner);


        if (i == 1) {
            //load
            DatabaseDesign ret = loadFromFile();

            System.out.println(ret.toString());
            return ret;

        } else {
            //create new design
            DatabaseDesign ret = newDesign();

            System.out.println(design.toString());
            return ret;
        }
    }

    private DatabaseDesign newDesign() {
        /*
         * specify DB-wide specifications
         */
        // DB page size
        dbPageSize = getIntFromUserInput(
                "Please enter DB Page size in bytes: ",
                scanner);
        design.setDBPageSize(dbPageSize);
        System.out.println("DB page size now set as: " + dbPageSize);

        // DB fill factor
        dbFillFactor = getDoubleFromUserInput(
                "Please enter average DB fill factor as proportion of each page in bytes: ",
                scanner);
        design.setDBFillFactor(dbFillFactor);
        System.out.println("DB fillfactor now set as: " + dbFillFactor);

        // relation average IO time
        double ioTime = getDoubleFromUserInput("Please enter average DB IO Time in milliseconds: ", scanner);
        design.setDAverageIOTime(ioTime);

        /*
        * specify relations
        */
        System.out.println("Now specifying RELATIONS ...");

        // constantly get new relations until user specifies 'NO MORE'
        relations.add(initRelation());
        while (getYesNoAnswerToQuestion("Specify another relation?")) {
            relations.add(initRelation());
        }
        design.setRelations(relations);

        System.out.println("RELATIONs have now all been specified.");
        //*/


        // specify transactions

        System.out.println("Now specifying TRANSACTIONs ...");
        transactions.add(initTransaction());
        // want another transaction?
        while (getYesNoAnswerToQuestion("Would you like to add another Transaction?")) {
            transactions.add(initTransaction());
        }
        design.setTransactions(transactions);
        System.out.println("TRANSACTIONs have now all been specified.");

        // save
        saveToFile(design);

        //close scanner
        scanner.close();

        // return database design
        return design;
    }

    /* debug code */

    private DatabaseDesign loadFromFile() {

        // specify filename
        String fileName = getStringFromUserInput("Please specify the name of the DatabaseDesign you wish to load:", scanner);
        fileName = "savedDesigns/" + fileName + ".dbdesign";
        // create FileInputStream for filename
        FileInputStream inputStream = null;
        try {
            inputStream = new FileInputStream(fileName);

            // create Object inputstream
            ObjectInputStream objectStream = new ObjectInputStream(inputStream);

            DatabaseDesign design = (DatabaseDesign) objectStream.readObject();

            return design;

        } catch (FileNotFoundException e) {

            System.out.println("Invalid filename\n");  //failed to create fileinputstream
            //try again with new filename
            return loadFromFile();

        } catch (IOException e) {
            e.printStackTrace();  // failed to create objectinputstream
        } catch (ClassNotFoundException e) {
            System.out.println("Database design could not be extracted");
            e.printStackTrace();  // couldnt cast object to a database design
        }

        return loadFromFile();

    }

    private void saveToFile(DatabaseDesign designToBeSaved) {

        String fileName = getStringFromUserInput("Please choose a filename for this Database Design:", scanner);

        // instantiate fileoutputstream to null
        FileOutputStream outputStream = null;
        try {
            // init fileoutputstream with new filename
            outputStream = new FileOutputStream("savedDesigns/" + fileName + ".dbdesign");
        } catch (FileNotFoundException e) {

            e.printStackTrace();
        }
        // constructor inject the fileoutputstream into DesignSave object
        saver = new DesignSave(designToBeSaved, outputStream);


    }

    /* end debug code */


    /**
     * processes user input and creates a Relation object
     *
     * @return Relation as per user spec
     */
    public Relation initRelation() {

        // instantiate new relation with relevant DBdesign numbers.
        // expected number of rows specified later.
        Relation newRel = new Relation(0, design.getDAverageIOTime(), design.getDBPageSize(), design.getDBFillFactor());


        // get and set relation name.
        String relationName = getStringFromUserInput("Please enter a name for your new Relation:", scanner);
        newRel.setRelationName(relationName);

        System.out.println("Creating the RELATION '" + relationName + "' ...");


        // get and set expected number of rows
        int expectedNumRows = getIntFromUserInput("Please enter the expected number of rows that this RELATION will contain:", scanner);
        newRel.setExpectedNumRows(expectedNumRows);

        // specify and then set attributes
        System.out.println("We now specify the set of ATTRIBUTES for the '" + relationName + "' RELATION...");
        HashSet<Attribute> attributesForRelation = new HashSet<Attribute>();
        attributesForRelation.add(initAttribute(relationName));
        while (getYesNoAnswerToQuestion("Does the RELATION '" + relationName + "' have more than the " + attributesForRelation.size() + " attribute(s) already entered?")) {
            attributesForRelation.add(initAttribute(relationName));
        }
        newRel.setAttributes(attributesForRelation);

        // specify indexes and then add the set to the RELATION
        HashSet<AbstractTableIndex> tableIndexesForRelation = new HashSet<AbstractTableIndex>();

        Attribute[] availableAttributes = new Attribute[attributesForRelation.size()];
        attributesForRelation.toArray(availableAttributes);
        String[] availableAttributeNames = new String[availableAttributes.length];
        for (int i = 0; i < availableAttributeNames.length; i++) {
            availableAttributeNames[i] = availableAttributes[i].getAttributeName();
        }
        System.out.println("Now that all ATTRIBUTES are known, we can define the INDEXs on RELATION '" + relationName + "' ...");


        // initialise index
        tableIndexesForRelation.add(initTableIndex(availableAttributes, availableAttributeNames));
        while (getYesNoAnswerToQuestion("Is there another INDEX defined on the RELATION '" + relationName + "'?")) {
            tableIndexesForRelation.add(initTableIndex(availableAttributes, availableAttributeNames));
        }
        newRel.setTableIndexes(tableIndexesForRelation);

        System.out.println("The RELATION '" + relationName + "' has now been fully specified.");
        return newRel;
    }

    private Attribute initAttribute(String relationName) {

        System.out.println("Specifying new ATTRIBUTE on RELATION '" + relationName + "' ...");

        Attribute result = new Attribute("Blah", 20);

        // specify name
        result.setAttributeName(getStringFromUserInput("Please enter a name for this ATTRIBUTE:", scanner));
        result.setAttributeSize(getIntFromUserInput("Please enter the SIZE of this ATTRIBUTE", scanner));

        return result;
    }

    private AbstractTableIndex initTableIndex(Attribute[] availableIndexes, String[] availableIndexNames) {
        // select attribute(s)
        LinkedList<Attribute> attributesForIndex = new LinkedList<Attribute>();

        int lastSelected = -1;

        // add getFirst relation visited
        lastSelected = getOptionFromUserInput(
                "Choose a ATTRIBUTE which forms part (or all) of this index ...",
                availableIndexNames,
                scanner);
        // add relation to visited list
        attributesForIndex.add(availableIndexes[lastSelected]);
        // constantly add more relations
        while (getYesNoAnswerToQuestion(
                "Is this INDEX composed of more ATTRIBUTES?")) {
            // add another relation visited
            lastSelected = getOptionFromUserInput(
                    "Choose another ATTRIBUTE which is part of the composite structure of this INDEX:",
                    availableIndexNames,
                    scanner);
            attributesForIndex.add(availableIndexes[lastSelected]);
        }

        // is index CLUSTERED?
        boolean clustered = getYesNoAnswerToQuestion("Is this index clustered?");

        // is index UNIQUE?
        boolean isUnique = getYesNoAnswerToQuestion("Is this a UNIQUE index?");

        // decide index type
        if (isUnique) {
            if (clustered) {
                return new UniqueClusteredTreeIndex(attributesForIndex);
            } else {
                return new UniqueUnclusteredTreeIndex(attributesForIndex);
            }
        } else {
            // not unique index

        }

        // create new TableIndex structure
        AbstractTableIndex index = new HeapIndex();
        index.setAttributeList(attributesForIndex);
        return index;
    }

    private Transaction initTransaction() {

        //init container for proc statements in transaction
        LinkedList<IProceduralStatement> proceduralStatements = new LinkedList<IProceduralStatement>();

        System.out.println("Specifying a new TRANSACTION ...");
        // get ID and increment
        int id = currentTransactionID;
        currentTransactionID++;

        // get user to input transaction prevalance
        int transactionPopulation = -1;
        while (transactionPopulation < 0) {
            transactionPopulation= getIntFromUserInput("Enter the number of this transaction",scanner);
        }

        // get user to input arrival rate
        double arrivalRate = -1;
        while (arrivalRate < 0) {
            arrivalRate = getDoubleFromUserInput("Enter mean inter-arrival time (\u03BB)", scanner);
        }

        // add statements to transaction
        System.out.println("TRANSACTIONs consist of one or more PROCEDURAL STATEMENTs - initialising getFirst PROCEDURAL STATEMENT ...");
        // new procedural statement -- this invokes helper function *** initProceduralStatement() ***
        proceduralStatements.add(initProceduralStatement());
        //need another procedural statement?
        while (getYesNoAnswerToQuestion("Would you like to add another PROCEDURAL STATEMENT to this TRANSACTION?")) {
            proceduralStatements.add(initProceduralStatement());
        }

        // init transaction with id and population
        Transaction result = new Transaction(false,Integer.toString(id),transactionPopulation);
        // add procedural statement set to the transaction
        result.setProceduralStatements(proceduralStatements);

        System.out.println("Transaction has been fully specified!");
        return result;
    }

    private double getDoubleFromUserInput(String userQuestion, Scanner scannerInUse) {
        System.out.println("\n" + userQuestion);
        double userSpecifiedDouble;
        try {
            userSpecifiedDouble = scannerInUse.nextDouble();
        } catch (InputMismatchException e) {
            System.out.println(e.getMessage());
            return getDoubleFromUserInput(userQuestion, scannerInUse);
        }
        return userSpecifiedDouble;

    }

    private boolean getYesNoAnswerToQuestion(String question) {
        int wantAnother = 0;

        wantAnother = getOptionFromUserInput(question,
                yesNoArray,
                scanner);
        System.out.println("Your selection was: " + yesNoArray[wantAnother]);


        return wantAnother == 1;
    }

    private int getOptionFromUserInput(String userQuestion, String[] options, Scanner scannerInUse) {

        System.out.println("\n" + userQuestion);

        System.out.println("\nType the number in square brackets to choose the option associated with it\n");

        // print out all options to stdout
        for (int i = 0; i < options.length; i++) {
            System.out.println("\t[" + i + "] " + options[i]);
        }

        int index = -1;

        index = scannerInUse.nextInt();

        if (index < 0 || index >= options.length) {
            return getOptionFromUserInput("Invalid Choice!\n" + userQuestion, options, scannerInUse);
        }

        return index;

    }

    private String getStringFromUserInput(String userQuestion, Scanner scannerInUse) {

        System.out.println("\n" + userQuestion);
        String userSpecifiedString = "";
        userSpecifiedString = scannerInUse.next();
        return userSpecifiedString;

    }

    private int getIntFromUserInput(String userQuestion, Scanner scannerInUse) {

        System.out.println("\n" + userQuestion);
        int userSpecifiedInt = -1;
        userSpecifiedInt = scannerInUse.nextInt();
        return userSpecifiedInt;

    }

    private IProceduralStatement initProceduralStatement() {

        System.out.println("Specifying a new PROCEDURAL STATEMENT");

        // specify whether loop, branch, or sequence of one or more SQL statements
        int selectedProcStmtType = getUserToSelectProceduralStatementType();

        switch (selectedProcStmtType) {

            case 0: // case: ProcStmtType = loop

                return initLoopProcStmt();

            case 1:// case: ProcStmtType = branch

                return initBranchProcStmt();

            case 2: // case: ProcStmtType = SQLStatementSequence

                return initSQLStatementSequenceProcStmt();

        }

        return null;
    }

    private Loop initLoopProcStmt() {

        System.out.println("Setting up this PROCEDURAL STATEMENT to be a LOOP ... ");

        // find out how many iterations the loop will contain
        int numIterations = getIntFromUserInput("How many iterations will this LOOP contain?", scanner);

        // recurse - call initProceduralStatement()
        System.out.println("We need to specify the body of this " + numIterations
                + " iteration LOOP using another PROCEDURAL STATEMENT: ");
        IProceduralStatement loopBody = initProceduralStatement();

        System.out.println("LOOP statement created!");

        // instantiate loop object with returned procedural statement
        return new Loop(numIterations, loopBody);

    }

    private Branch initBranchProcStmt() {

        System.out.println("Setting up this PROCEDURAL STATEMENT to be a BRANCH ... ");
        // get probability of true branch being executed
        double trueProb = getDoubleFromUserInput("Please specify the probability that the condition on this BRANCH will evaluate to TRUE", scanner);

        // recurse to get true branch and false branch
        System.out.println("Now we specify the SEQUENCE OF SQL STATEMENTS executed if the BRANCH condition evaluates to TRUE...");

        // only allow Sequence of SQLStatementSequence on each branch
        IProceduralStatement trueStat = initSQLStatementSequenceProcStmt();
        System.out.println("Now we specify the SEQUENCE OF SQL STATEMENTS executed if the BRANCH condition evaluates to FALSE...");
        IProceduralStatement falseStat = initSQLStatementSequenceProcStmt();

        System.out.println("BRANCH statement created!");

        // instantiate branch object with returned procedural statement
        return new Branch(trueProb, trueStat, falseStat);

    }

    private SQLQuerySequence initSQLStatementSequenceProcStmt() {
        // instantiate SQLStatementSequence object
        SQLQuerySequence sequence = new SQLQuerySequence();

        // process addition of SQL statements
        System.out.println("Setting up this PROCEDURAL STATEMENT to be a SEQUENCE OF SQL STATEMENTS ... ");

        int numStatements = 0;

        System.out.println("Instantiate getFirst SQL STATEMENT ...");

        // create array of names of available relations for query to visit
        Relation[] availableRelations = new Relation[relations.size()];
        relations.toArray(availableRelations);
        String[] relationNames = new String[availableRelations.length];
        for (int i = 0; i < relationNames.length; i++) {
            relationNames[i] = availableRelations[i].getRelationName();
        }

        // constantly add queries
        sequence.addSQLStatement(specifySQLQuery(availableRelations, relationNames));
        numStatements++;
        while (getYesNoAnswerToQuestion("Add another SQL STATEMENT to this SEQUENCE OF SQL STATEMENTS?")) {
            sequence.addSQLStatement(specifySQLQuery(availableRelations, relationNames));
            numStatements++;
        }

        System.out.println("SQL Statements instantiated, SEQUENCE OF " + numStatements + " SQL STATEMENTS created!...\n");
        // return SQL statement sequence
        return sequence;

    }

    private SQLQuery specifySQLQuery(Relation[] availableRelations, String[] availableRelationsNames) {

        // allow multiple visits to same table
        // specify relation visited per query - only allow one table per query

        // reference pointer to the desired user selected relation
        int lastSelected = -1;

        // add relation visited
        lastSelected = getOptionFromUserInput(
                "Choose a RELATION which this SQL STATEMENT visits getFirst ...",
                availableRelationsNames,
                scanner);
        // add relation to visited list
        HashSet<Attribute> attributes = getUserToSpecifyAttributesInRelationForTransaction(availableRelations[lastSelected]);

        SerializablePair<Relation, HashSet<Attribute>> relationsVisited = new SerializablePair<Relation, HashSet<Attribute>>(availableRelations[lastSelected], attributes);

        // specify type of query
        SQLQuery query = initSQLQuery(relationsVisited);

        return query;
    }

    private HashSet<Attribute> getUserToSpecifyAttributesInRelationForTransaction(Relation relation) {

        HashSet<Attribute> attributes = relation.getAttributes();

        // create array of names of available attributes for query to condition on
        Attribute[] availableAttributes = new Attribute[attributes.size()];
        attributes.toArray(availableAttributes);
        // pluck out attribute names
        String[] attributeNames = new String[availableAttributes.length];
        for (int i = 0; i < attributeNames.length; i++) {
            attributeNames[i] = availableAttributes[i].getAttributeName();
        }

        //initialise attributes
        HashSet<Attribute> chosenAttributes = new HashSet<Attribute>();

        int lastSelected = -1;

        // add getFirst attribute
        lastSelected = getOptionFromUserInput(
                "Choose a ATTRIBUTE which this SQL STATEMENT uses in its WHERE condition ...",
                attributeNames,
                scanner);
        // add relation to visited list
        chosenAttributes.add(availableAttributes[lastSelected]);
        // constantly add more attribute
        while (getYesNoAnswerToQuestion(
                "Does this SQL STATEMENT condition on another ATTRIBUTE in this relation? ")) {
            // add another relation visited
            lastSelected = getOptionFromUserInput(
                    "Choose another ATTRIBUTE which this SQL STATEMENT uses in its WHERE condition ...",
                    attributeNames,
                    scanner);

            chosenAttributes.add(availableAttributes[lastSelected]);
        }

        return chosenAttributes;


    }

    private SQLQuery initSQLQuery(SerializablePair<Relation, HashSet<Attribute>> relationHashSetPair) {

        int queryType = -1;

        // for each relation in a DML query, specify attributes of WHERE clause

        queryType = getOptionFromUserInput(
                "What type of SQL STATEMENT is this?",
                sqlStatementTypesArray,
                scanner
        );


        switch (queryType) {

            case 0:
                return new SequentialScanQuery(relationHashSetPair);

            case 1:

                EqualitySearchQuery ret = new EqualitySearchQuery(relationHashSetPair);

                return ret;

            case 2:
                // specify number of matching rows
                int numRows2 = getUserToSpecifyNumMatchingRows();
                EqualityWithRangeSearchQuery ret2 = new EqualityWithRangeSearchQuery(relationHashSetPair);
                ret2.setNumMatchingRows(numRows2);

                return ret2;

            case 3:
                // specify number of matching rows
                int numMatchingRows = getUserToSpecifyNumMatchingRows();
                return new UpdateDeleteQuery(numMatchingRows, relationHashSetPair);

            default:
                return new InsertQuery(relationHashSetPair);

        }
    }

    private int getUserToSpecifyNumMatchingRows() {

        return getIntFromUserInput("Please specify the number of matching rows for this QUERY:", scanner);

    }

    /**
     * creates user prompt and handles user input to enter a procedural statement type
     *
     * @return int representing the intended type of procedural statement
     */
    private int getUserToSelectProceduralStatementType() {

        int selectedProcStmtType = -1;
        String[] proceduralStatementTypes = {"Loop", "Branch", "Sequence of one or more SQL statements"};

        selectedProcStmtType = getOptionFromUserInput("Please select the type of this PROCEDURAL STATEMENT:",
                proceduralStatementTypes,
                scanner);

        return selectedProcStmtType;

    }

    public DatabaseDesign getDesign() {
        return design;
    }

    public void setDesign(DatabaseDesign design) {
        this.design = design;
    }
}